/*
 * Tigase Jabber/XMPP Server
 * Copyright (C) 2004-2012 "Artur Hefczyc" <artur.hefczyc@tigase.org>
 *
 * This program is free software: you can redistribute it and/or modify
 * it under the terms of the GNU Affero General Public License as published by
 * the Free Software Foundation, version 3 of the License.
 *
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU Affero General Public License for more details.
 *
 * You should have received a copy of the GNU Affero General Public License
 * along with this program. Look for COPYING file in the top folder.
 * If not, see http://www.gnu.org/licenses/.
 *
 * $Rev$
 * Last modified by $Author:$
 * $Date$
 *
 */
package tigase.server.xmppclient;

//~--- non-JDK imports --------------------------------------------------------

import tigase.util.TigaseStringprepException;
import tigase.xmpp.BareJID;

import tigase.db.DBInitException;
import tigase.db.DataRepository;
import tigase.db.RepositoryFactory;
import tigase.db.UserNotFoundException;

//~--- JDK imports ------------------------------------------------------------

import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.Map;
import java.util.concurrent.ConcurrentSkipListMap;
import java.util.logging.Level;
import java.util.logging.Logger;

//~--- classes ----------------------------------------------------------------

/**
 * Extended implementation of SeeOtherHost using redirect information from
 * database
 * 
 */
public class SeeOtherHostDB extends SeeOtherHostHashed {

	private static final Logger log = Logger.getLogger(SeeOtherHostDB.class.getName());

	public static final String SEE_OTHER_HOST_TABLE = "tig_see_other_hosts";
	public static final String SEE_OTHER_HOST_DB_URL_KEY = CM_SEE_OTHER_HOST_CLASS_PROP_KEY
			+ "/" + "db-url";
	public static final String SEE_OTHER_HOST_DB_QUERY_KEY =
			CM_SEE_OTHER_HOST_CLASS_PROP_KEY + "/" + "get-host-query";
	public static final String DB_GET_ALL_DATA_DB_QUERY_KEY =
			CM_SEE_OTHER_HOST_CLASS_PROP_KEY + "/" + "get-all-data-query";
	public static final String GET_ALL_QUERY_TIMEOUT_QUERY_KEY =
		CM_SEE_OTHER_HOST_CLASS_PROP_KEY + "/" + "get-all-query-timeout";

	
	public static final String SERIAL_ID = "sid";
	public static final String USER_ID = "uid";
	public static final String NODE_ID = "node_id";

	public static final String DEF_DB_GET_HOST_QUERY = " select * from tig_users, "
			+ SEE_OTHER_HOST_TABLE + " where tig_users.uid = " + SEE_OTHER_HOST_TABLE + "."
			+ USER_ID + " and user_id = ?";

	private static final String DEF_DB_GET_ALL_DATA_QUERY =
			"select user_id, node_id from tig_users, " + SEE_OTHER_HOST_TABLE
					+ " where tig_users.uid = " + SEE_OTHER_HOST_TABLE + "." + USER_ID;

	private static final String CREATE_STATS_TABLE = "create table " + SEE_OTHER_HOST_TABLE
			+ " ( " + SERIAL_ID + " serial," + USER_ID + " bigint NOT NULL, "
			+ NODE_ID + " varchar(2049) NOT NULL, " + " primary key (" + SERIAL_ID + "), "
			+ " constraint tig_see_other_host_constr foreign key (" + USER_ID
			+ ") references tig_users (" + USER_ID + ")" + ")";

	private static final String DERBY_CREATE_STATS_TABLE = "create table " + SEE_OTHER_HOST_TABLE
			+ " ( " + SERIAL_ID + " bigint generated by default as identity not null," + USER_ID + " bigint  NOT NULL, "
			+ NODE_ID + " varchar(2049) NOT NULL, " + " primary key (" + SERIAL_ID + "), "
			+ " constraint tig_see_other_host_constr foreign key (" + USER_ID
			+ ") references tig_users (" + USER_ID + ")" + ")";

	private static final String SQLSERVER_CREATE_STATS_TABLE = "create table " + SEE_OTHER_HOST_TABLE
			+ " ( " + SERIAL_ID + " [bigint] IDENTITY(1,1)," + USER_ID + " bigint NOT NULL, "
			+ NODE_ID + " nvarchar(2049) NOT NULL, " + " primary key (" + SERIAL_ID + "), "
			+ " constraint tig_see_other_host_constr foreign key (" + USER_ID
			+ ") references tig_users (" + USER_ID + ")" + ")";


	private static final int DEF_QUERY_TIME_OUT = 0;

	private String get_host_query = DEF_DB_GET_HOST_QUERY;
	private String get_all_data_query = DEF_DB_GET_ALL_DATA_QUERY;
	private String get_host_DB_url = "";

	private DataRepository data_repo = null;
	private Map<BareJID, BareJID> redirectsMap =
			new ConcurrentSkipListMap<BareJID, BareJID>();

	// Methods

	@Override
	public BareJID findHostForJID(BareJID jid, BareJID host) {

		
		
		BareJID see_other_host = redirectsMap.get(jid);
		if (see_other_host != null) {
			return see_other_host;
		} else {
			see_other_host = host;
		}

		try {
			see_other_host = queryDB(jid);
		} catch (Exception ex) {
			see_other_host = super.findHostForJID(jid, host);
			log.log(Level.SEVERE, "DB lookup failed, fallback to SeeOtherHostHashed: ", ex);
		}

		return see_other_host;
	}

	@Override
	public void getDefaults(Map<String, Object> defs, Map<String, Object> params) {
		super.getDefaults(defs, params);

		if (params.containsKey("--user-db-uri")) {
			get_host_DB_url = (String) params.get("--user-db-uri");
		}
		defs.put(SEE_OTHER_HOST_DB_URL_KEY, get_host_DB_url);
		defs.put(SEE_OTHER_HOST_DB_QUERY_KEY, get_host_query);
		defs.put(DB_GET_ALL_DATA_DB_QUERY_KEY, get_all_data_query);
	}

	@Override
	public void setProperties(Map<String, Object> props) {
		super.setProperties(props);

		if ((props.containsKey(SEE_OTHER_HOST_DB_URL_KEY))
				&& !props.get(SEE_OTHER_HOST_DB_URL_KEY).toString().trim().isEmpty()) {
			get_host_DB_url = props.get(SEE_OTHER_HOST_DB_URL_KEY).toString().trim();
		}
		props.put(SEE_OTHER_HOST_DB_URL_KEY, get_host_DB_url);

		if ((props.containsKey(SEE_OTHER_HOST_DB_QUERY_KEY))
				&& !props.get(SEE_OTHER_HOST_DB_QUERY_KEY).toString().trim().isEmpty()) {
			get_host_query = props.get(SEE_OTHER_HOST_DB_QUERY_KEY).toString().trim();
		}
		props.put(SEE_OTHER_HOST_DB_QUERY_KEY, get_host_query);

		if ((props.containsKey(DB_GET_ALL_DATA_DB_QUERY_KEY))
				&& !props.get(DB_GET_ALL_DATA_DB_QUERY_KEY).toString().trim().isEmpty()) {
			get_all_data_query = props.get(DB_GET_ALL_DATA_DB_QUERY_KEY).toString().trim();
		}
		props.put(DB_GET_ALL_DATA_DB_QUERY_KEY, get_all_data_query);

		try {
			initRepository(get_host_DB_url, null);
		} catch (Exception ex) {
			log.log(Level.SEVERE, "Cannot initialize connection to database: ", ex);
		}
	}

	public void initRepository(String conn_str, Map<String, String> map)
			throws SQLException, ClassNotFoundException, IllegalAccessException,
			InstantiationException, DBInitException {

		log.log(Level.INFO, "Initializing dbAccess for db connection url: {0}", conn_str);
		data_repo = RepositoryFactory.getDataRepository(null, conn_str, map);

		checkDB();

		data_repo.initPreparedStatement(get_host_query, get_host_query);
		data_repo.initPreparedStatement(get_all_data_query, get_all_data_query);
		queryAllDB();
	}

	/**
	 * Performs database check, creates missing schema if necessary
	 *
	 * @throws SQLException
	 */
	private void checkDB() throws SQLException {
		DataRepository.dbTypes databaseType = data_repo.getDatabaseType();
		switch ( databaseType ) {
			case derby:
				data_repo.checkTable( SEE_OTHER_HOST_TABLE, DERBY_CREATE_STATS_TABLE );
				break;
			case jtds:
			case sqlserver:
				data_repo.checkTable( SEE_OTHER_HOST_TABLE, SQLSERVER_CREATE_STATS_TABLE );
				break;
			case postgresql:
			case mysql:
			default:
				data_repo.checkTable( SEE_OTHER_HOST_TABLE, CREATE_STATS_TABLE );
				break;
		}
	}

	private BareJID queryDB(BareJID user) throws UserNotFoundException, SQLException,
			TigaseStringprepException {

		PreparedStatement get_host = data_repo.getPreparedStatement(user, get_host_query);

		ResultSet rs = null;

		synchronized (get_host) {
			try {
				get_host.setString(1, user.toString());

				rs = get_host.executeQuery();

				if (rs.next()) {
					return BareJID.bareJIDInstance(rs.getString(NODE_ID));
				} else {
					throw new UserNotFoundException("Item does not exist for user: " + user);
				} // end of if (isnext) else
			} finally {
				data_repo.release(null, rs);
			}
		}
	}

	private void queryAllDB() throws SQLException {

		PreparedStatement get_all = data_repo.getPreparedStatement(null, get_all_data_query);
		get_all.setQueryTimeout(DEF_QUERY_TIME_OUT);

		ResultSet rs = null;

		synchronized (get_all) {
			try {
				rs = get_all.executeQuery();

				while (rs.next()) {
					String user_jid = rs.getString("user_id");
					String node_jid = rs.getString(NODE_ID);
					try {
						BareJID user = BareJID.bareJIDInstance(user_jid);
						BareJID node = BareJID.bareJIDInstance(node_jid);
						redirectsMap.put(user, node);
					} catch (TigaseStringprepException ex) {
						log.warning("Invalid user's or node's JID: " + user_jid + ", " + node_jid);
					}
				} // end of if (isnext) else
			} finally {
				data_repo.release(null, rs);
			}
		}
		
		log.info("Loaded " + redirectsMap.size() + " redirect definitions from database.");
	}
}
